<?php

/***  DOCUMENTATION LAYER

Klenwell LAMP Backup

Name: LampBack
Version: 0.7
Last Update: Jun 2007
Author: Tom at klenwell@gmail.com

DESCRIPTION
	A class for backing up a mysql database

METHODS
	MAGIC
	LampBack($debug=0, $oid=null)		*php 4 constructor*
	__construct($debug, $oid)					*php 5 constructor*
	__destruct()	
	
	PUBLIC
	export()
	send_mail($body='', $subject='', $attach_file='')
	print_d($message, $color='c33')
	print_r()
	dump()
	
	PRIVATE
	_system($cmd_str)
	_delete_file($fpath)
	_prepare_mail_message($fpath, $db_name, $TABLES, $tx)
	_script_timer()
	_reset_script_timer()	
	_system_check()
	_db_check()
	_os_check()
	_die($msg='')
	_scrub_pw($string)
	_trigger_error($msg)
	_set_session_data()
	_get_session_data()
	_has_session_data()
	_set_filename()
	_set_dirpath()
	
USAGE
	see packaged tests

REQUIREMENTS
	system() function must be enabled and your db user must have sufficient privileges
	make sure the target directory for the created sql/gz files are writeable (755 or 777)
	
NOTES
	Can be called directly (useful for testing) but intended for cronjob usage.  To set 
	cronjob on Cpanel:
			
	/usr/bin/php /usr/home/$username/.../backup_cron.php > /usr/home/$username/.../lampback.log
		
	Sample DATABASE settings:
		1) 'db1' => '*', 		// backs up selected table inside the array
		2) 'db2' => array('table1', 'table2'), 		// backs up all tables in database
			
	Note: keys (database names) must be different -- if duplicated, only the last one will run
	
	Adapted in part from http://blog.tvlgiao.com/2006/05/09/php-cron-job-backups.html	

______________________________________________________________________________*/

// Load File of Base Class
$base_fname = '';
$base_dirpath = dirname(__FILE__) . DIRECTORY_SEPARATOR;
#require_once($base_dirpath . $base_fname);


// LampBack
/*____________________________________________________________________________*/
class LampBack
{
/* PUBLIC PROPERTIES */
var $debug = 0;
var $class_name = __CLASS__;
var $oid = 'LampBack';
var $DS = DIRECTORY_SEPARATOR;

var $DATABASE = array();
var $db_user = '';
var $db_pass = '';
var $db_host = 'localhost';
var $mail_to = '';
var $mail_from = 'klenwell-lampback-mailer';
var $tar_on = 1;
var $mail_on = 1;
var $save_files = 0;

// mysqldump options (see http://dev.mysql.com/doc/refman/4.1/en/mysqldump.html)
var $md_opt = '--opt --quote-names';

var $is_win = 0;
var $mt_start = 0;

// paths and dirs
var $mysqldump_path = '/usr/local/bin/mysqldump';
var $save_dir = '';
var $logdir = '';
var $err_log = 'err_log';

/* PRIVATE PROPERTIES */
var $_filename = '';
var $_dirpath = '';


/* ** MAGIC METHODS ** */
// php4 constructor
function LampBack($debug=0, $oid=null)
{
	$this->__construct($debug, $oid);
	register_shutdown_function( array($this, '__destruct') );
}
// END constructor

// php5 constructor
function __construct($debug=0, $oid=null)
{
	// default
	$this->debug = $debug;
	$this->oid = ( empty($oid) ) ? $this->class_name : $oid;
	$this->_set_filename();
	$this->_set_dirpath();
	
	// additional code
	$this->_script_timer();
	$this->err_log = $this->logdir . 'error.log';
	
	// debug
	if ( $this->debug ) $this->print_d('debugging is active for oid ' . $this->oid);
	if ( $this->debug ) $this->print_d('constructor complete for class ' . __CLASS__);
}
// END constructor

// destructor
function __destruct()
{
	if ( $this->debug ) $this->print_d("destroying class {$this->class_name} (oid: {$this->oid})");
}
// END destructor



/* ** PUBLIC METHODS ** */
// method: export
function export()
{
	$dbs_exported = 0;		// returns number of dbs exported
	
	// prescript checks
	$this->_system_check();
	$this->_db_check();
	$this->_os_check();
	
	// build login cmd
	$login_cmd = "-u {$this->db_user} -p{$this->db_pass}";
	
	// cycle through dbs
	foreach ( $this->DATABASE as $db_name => $_TABLE )
	{
		// check script timer
		$this->_script_timer();
		
		// set tables
		if ( $_TABLE == '*' )
		{
			$tbl_cmd = '';
		}
		elseif ( is_array($_TABLE) )
		{
			$tbl_list = implode(' ', $_TABLE);
			$tbl_cmd = " --tables $tbl_list";
		}
		else
		{
			$this->_trigger_error("invalid table settings for db $db_name, will back up all tables", E_USER_WARNING);
			$tbl_cmd = '';
		}
		
		// set files names
		$sql_file = $this->save_dir . $db_name . '.sql';
		$tar_file = $this->save_dir . $db_name . '.tar.gz';
		
		// build mysqldump command
		$sql_cmd = "{$this->mysqldump_path} {$this->md_opt} $login_cmd -B $db_name{$tbl_cmd} > {$sql_file}";
		
		// run system command
		if ( $this->_system($sql_cmd) ) 
		{
			$dbs_exported++;
		}
		else
		{
			$this->_delete_file($sql_file);
			continue;
		}
		
		// post-system checks
		if ( is_file($sql_file) )
		{
			$cycle_tx = $this->_script_timer();
			$this->_reset_script_timer();
			if ( $this-> debug ) $this->print_d("db <b>{$db_name}</b> backed up in <b>{$cycle_tx}</b> s", 'green');			
		}
		else
		{
			$this->_trigger_error("file [<b>{$sql_file}</b>] not found", E_USER_WARNING);
			continue;
		}
		
		// tar
		$tar_cmd = "tar -zcf {$tar_file} {$sql_file}";
		if ( !$this->is_win && $this->tar_on ) $this->_system($tar_cmd);
		
		// mail
		if ( $this->mail_on )
		{
			$attach_file = ( $this->tar_on && !$this->is_win ) ? $tar_file : $sql_file;
		
			$body = $this->_prepare_mail_message($attach_file, $db_name, $_TABLE, $cycle_tx);
			$subject = 'lampback backup for db ' . $db_name . ' on ' . date('Y-m-d H:i:s');
			
			if ( $this->send_mail($body, $subject, $attach_file) )
			{
				if ( $this->debug ) $this->print_d("file '{$attach_file}' mailed to {$this->mail_to}");
			}
			else
			{
				$this->_trigger_error('failed to mail file', E_USER_WARNING);
			}
		}
		
		// remove files
		if ( !$this->save_files ) $this->_delete_file($sql_file);
		if ( !$this->save_files && $this->tar_on ) $this->_delete_file($tar_file);
	}
	
	return $dbs_exported;
}
// END method

// method: mail
function send_mail($body='', $subject='', $attach_file='')
{
	$is_sent = 0;		// return
	
	$boundary = '-----=' . md5( uniqid ( rand() ) );
	
	if ( $attach_file ) 
	{
		$basename = basename($attach_file);
		$message .= "--".$boundary."\n";
		$message .= "Content-Type: application/binary; name=\"$basename\"\n";
		$message .= "Content-Transfer-Encoding: base64\n";
		$message .= "Content-Disposition: attachment; filename=\"$basename\"\n\n";
		
		$content = file_get_contents($attach_file);
		$content_encode = chunk_split(base64_encode($content));
		$message .= $content_encode . "\n\n";
	}
	
	if ( $body ) 
	{
		// Text Version
		$message .= "--".$boundary."\n";
		$message .= "Content-Type: text/plain; charset=iso-8859-1\n";
		$message .= "Content-Transfer-Encoding: 8bit\n";
		$message .= "This is a multi-part message in MIME format.\n\n";
		$message .= strip_tags($body)."\n\n";

		// HTML Version
		$message .= "--".$boundary."\n";
		$message .= "Content-Type: text/html; charset=iso-8859-1\n";
		$message .= "Content-Transfer-Encoding: 8bit\n\n";
		$message .= $body."\n\n"; 
	}
	
	// finished
	$message .= "--".$boundary."--\n\n";  // finish with two eol's for better security
	
	if ( $this->debug ) $this->print_d("sending message:<pre>$body</pre>");
	
	$headers = '';
	if ( $this->mail_from ) $headers .= "From: {$this->mail_from}" . "\r\n";
	$headers .= "MIME-Version: 1.0" . "\r\n";
	$headers .= "Content-Type: multipart/mixed; boundary=\"$boundary\"";

	if ( mail($this->mail_to, $subject, $message, $headers) ) $is_sent = 1;
	
	return $is_sent;
}
// END method

// method: print_d
function print_d($message, $color='c33')
{
	$out = "<div style='line-height:1.5em; font-family:monospace; color:$color;'>$message</div>";
	echo $out;
	return;
}
// END method

// method: print_r
function print_r($Mixed)
{
	$return = htmlspecialchars(print_r($Mixed, 1));
	$return = "<pre>$return</pre>";
	return $return;
}
// END method

// method: dump
function dump()
{
	echo $this->print_r($this);
	return;
}
// END method



/* ** PRIVATE METHODS ** */
// method: run system function (see http://www.php.net/manual/en/function.system.php)
function _system($cmd_str)
{
	$succeeded = 0;		// return
	$sys_return = 0;
	
	if ( $this->debug ) $this->print_d('system command string: <code>' . $this->_scrub_pw($cmd_str) . '</code>', '#6666cc');
	
	// false = failure; 0 = no errors
	if ( system($cmd_str, $sys_return) === false )
	{
		$this->_trigger_error("system command [$cmd_str] failed");
		return 0;
	}
	
	// errors?
	if ( $sys_return > 0 )
	{
		$this->_trigger_error("there was a problem with system command [$cmd_str] -> system returned: {$sys_return}", E_USER_WARNING);
		$error_result = trim(file_get_contents($this->err_log));
		if ( empty($error_result) ) $error_result = '*empty*';
		$this->print_d("{$this->err_log}: <pre style='margin:0;'>$error_result</pre>", 'magenta');
	}
	else
	{
		if ( $this->debug ) $this->print_d("system returned: {$sys_return} (0 means no errors)", 'green');
		$succeeded = 1;
	}
	
	return $succeeded;		
}
// END method

// method: delete
function _delete_file($fpath)
{
	if ( @unlink($fpath) )
	{
		if ( $this->debug ) $this->print_d("deleted file [$fpath]", 'orange');
		return 1;
	}
	else
	{
		if ( $this->debug ) $this->print_d("unable to delete file [$fpath]");
		return 0;
	}
}
// END method

// method: prepare mail message
function _prepare_mail_message($fpath, $db_name, $TABLES, $tx)
{
	$body = '';		// return
	
	// message data
	$tables = print_r($TABLES,1);
	$gmt = gmdate('Y-m-d h:i:s');
	
	$body = <<<TEXT
AMVC MYSQL BACKUP
DB: $db_name
TABLES: 
$tables

attached file: {$fpath} 

backed up in $tx s
gmt: $gmt
TEXT;
	
	return $body;
}
// END message

// method: script timer
function _script_timer()
{
	$time_lapse = '';		// return
	
	if ( empty($this->mt_start) )
	{
		$this->mt_start = microtime();
		return 0;
	}
	
	$t1 = $this->mt_start;
	$t2 = microtime();
	$time_lapse = number_format(((substr($t2,0,9)) + (substr($t2,-10)) - (substr($t1,0,9)) - (substr($t1,-10))),4);
	
	return $time_lapse;
}
// END method

// method: reset script timer
function _reset_script_timer()
{
	$this->mt_start = microtime();
}
// END method

// method: systems checks: mysqldump file, server fx
function _system_check()
{
	if ( $this->debug ) $this->print_d(__FUNCTION__ . ' in dev');
	
	// check mysqldump exists 
	if ( 0 && !is_file($this->mysqldump_path) )
	{
  	$this->_die("mysqldump file [<b>{$this->mysqldump_path}</b>] not found");
	}
	
	// check system fx available
	if ( !is_callable('system') )
	{
		$this->_die("'system' function not callable on this server");
	}
	
	return;
}
// END method

// method: database check
function _db_check()
{
	// check database array
	if ( empty($this->DATABASE) || !is_array($this->DATABASE) )
  {
  	$this->_die('no databases set -- please update config settings');
  }
	
	// check login settings
	if ( empty($this->db_user) || empty($this->db_pass) )
	{
		$this->_die('user name or password empty -- please update config settings');
	}
	
	// simple connection test
	if ( !mysql_connect($this->db_host, $this->db_user, $this->db_pass) )
	{
		$this->_die('unable to connect to server -- check settings!');
	}
	
	if ( $this->debug ) $this->print_d('database check passed', 'green');
}
// END method

// method: operating system check
function _os_check()
{
	if ( substr(PHP_OS,0,3) == 'WIN') 
	{
		$this->is_win = 1;
		if ( $this->debug ) $this->print_d('this script is running on a Windows server.  be advised, it is optimized for linux, but may still work.  be sure to set the right path to <b>mysqldump.exe</b>.  and you will have to delete any files created manually.', 'magenta');
	}
	else
	{
		$this->is_win = 0;
	}
}
// END method

// method: die
function _die($msg='')
{
	if ( empty($msg) ) $msg = 'no message';
		else $msg = $this->_scrub_pw($msg);
	trigger_error($msg, E_USER_ERROR);
	die("<h4 style='color:red;'>script aborted</h4>");
}
// END method

// method: scrub password
function _scrub_pw($string)
{
	return preg_replace('% -p[^\s]*\s%', ' -p****** ', $string);
}
// END method

// method: trigger error
function _trigger_error($msg)
{
	$msg = $this->_scrub_pw($msg);
	trigger_error($msg, E_USER_WARNING);
	return;
}
// END method

// method: _set_session_data
function _set_session_data()
{
	// initialize session
	if ( !session_id() ) session_start(); 
	$_SESSION[$this->oid] = array();
	
	// add session data here
	
	return;
}
// END method

// method: get session data
function _get_session_data()
{
	// initialize session
	if ( !$this->_has_session_data() ) return; 
		
	// retrieve session variables
	// $this->var = $_SESSION[$this->oid]['var'];
	
	return;
}
// END method

// method: has session data
function _has_session_data()
{
	// initialize session
	if ( !session_id() ) session_start(); 
		
	// retrieve session variables
	if ( empty($_SESSION[$this->oid]) ) return 0;
	else return 1;
}
// END method

function _set_filename() { $this->_filename = basename(__FILE__); }
function _set_dirpath() { $this->_dirpath = dirname(__FILE__) . $this->DS; }

} // end class
/*____________________________________________________________________________*/

?>
